#include "sys.h"
#include "net.h"
#include "log.h"
#include "uentry.h"
#include "us_ctl.h"
#include "udp_speed.h"

#define DEFAULT_RETRY_COUNT 10
static int PMTU = 1432;

static int check_pmtu(int fd)
{
#define MAX_PMTU_SIZE 0xffff
#define MAX_ETH_MTU 1500
#define MIN_PMTU_SIZE 576
#define IP_UDP_HEADER_LEN 28

    int pmtu = 0;
    int ret;
    char buffer[MAX_PMTU_SIZE] = UCTL_CONF_CMD_ECHO;
    int len = MAX_ETH_MTU - IP_UDP_HEADER_LEN;
    int i = 0;

    while (1) {

        i = 0;
        while (i++ < 5) {
            usleep(1000 * 100);
            ret = write(fd, buffer, len);
            if (ret < 0) {
                if (errno == EMSGSIZE) {
                    log_trace("check pmtu write message too loog,len:%d.", len);
                    continue;
                }
                log_syserr("check pmtu write error. len:%d", len);
                return 0;
            }
        }

        ret = read(fd, buffer, len);
        if (ret > 0) {
            pmtu = ret;
            break;
        }
        if (len-- < MIN_PMTU_SIZE)
            return 0;
    }
    while (1) {
        ret = read(fd, buffer, len);
        if (ret < 0)
            break;
    }
    return pmtu;
}

static float cal_rate(uint64_t bytes, int ms_time)
{
    return (float)(bytes * 8 / ((double)ms_time / 1000.0));
}
static int cli_ctl_snd(int fd, uctl_conf_t* uconf, uctl_conf_t* resp_uconf, int retry)
{
    int ret;
    char rcv_buffer[UDP_BUFFER_MAX] = { 0 }, snd_buf[UDP_BUFFER_MAX] = { 0 };

    uctl_conf_struct2str(uconf, snd_buf, sizeof(snd_buf));
    while (1) {
        ret = write(fd, snd_buf, strlen(snd_buf));
        if (ret < 0) {
            log_syserr("send error.");
            return 0;
        }
        log_trace("cli send %s", snd_buf);

        if (resp_uconf) {
            bzero(rcv_buffer, sizeof(rcv_buffer));
            ret = read(fd, rcv_buffer, sizeof(rcv_buffer));
            if (ret > 0) {
                log_trace("cli rcv resp:%s", rcv_buffer);
                uctl_conf_str2struct(resp_uconf, rcv_buffer, ret);
                return 1;
            }

        } else {
            break;
        }
        if (!retry--)
            return 0;
    }
    return 1;
}

static int cli_request(int ctl_fd, uint16_t* udp_port, int retry)
{
    uctl_conf_t uconf = { 0 }, resp_uconf = { 0 };

    strcpy(uconf.cmd, UCTL_CONF_CMD_REQ);
    if (cli_ctl_snd(ctl_fd, &uconf, &resp_uconf, DEFAULT_RETRY_COUNT)) {
        *udp_port = resp_uconf.port;
        return 1;
    }
    return 0;
}

static int cli_download(int fd, udpspeed_cli_conf_t* cli_conf)
{
    int ret, retry_count = DEFAULT_RETRY_COUNT;
    uint64_t total_bytes = 0, total_packets = 0;
    char buffer[UDP_BUFFER_MAX] = { 0 };
    char tmp_str[32] = { 0 }, bytes_str[32] = { 0 };
    uint64_t last_seq;
    uctl_conf_t uconf = { 0 };
    uentry_trans_info_t download_info = { 0 };
    struct timeval start_tm, end_tm;
    int diff_tm;

    strcpy(uconf.cmd, UCTL_CONF_CMD_DOWNLOAD);
    uconf.rate = cli_conf->target_rate;
    uconf.duration = cli_conf->duration;
    uconf.blksize = PMTU;

retry:
    cli_ctl_snd(fd, &uconf, NULL, 0);

    gettimeofday(&start_tm, NULL);
    bzero(&download_info, sizeof(download_info));
    while (1) {
        ret = read(fd, buffer, sizeof(buffer));
        if (ret == 0 || ret < 0)
            break;
        gettimeofday(&end_tm, NULL);
        download_info.bytes += ret;
        download_info.packets++;
        last_seq = *((uint64_t*)buffer);
    }
    diff_tm = TV2MS(&end_tm) - TV2MS(&start_tm);
    log_trace("diff_tm:%d", diff_tm);
    download_info.rate = cal_rate(download_info.bytes, diff_tm);
    bps2str(download_info.rate, tmp_str);
    byte2str(download_info.bytes, bytes_str);
    log_trace("total_bytes %s, total_packets:%lld", bytes_str, download_info.packets);
    log_trace("last_seq:%llu", last_seq);
    log_trace("lost percent:%.2f%%", Positive((1 - ((double)download_info.packets / last_seq)) * 100));
    log_trace("download rate:%s", tmp_str);
    if (download_info.bytes == 0 && retry_count > 0) {
        retry_count--;
        goto retry;
    }
    return 1;
}
static int cli_upload(int udp_fd, udpspeed_cli_conf_t* cli_conf)
{
    int ret;
    char buffer[UDP_BUFFER_MAX] = { 0 };
    char tmp_str[32] = { 0 };
    struct timeval start, now, end;
    double ms_start, ms_now, ms_end;
    uint64_t total_bytes = 0, total_packets = 0;
    uctl_conf_t uconf = { 0 }, uconf2 = { 0 }, resp_uconf = { 0 };
    uentry_trans_info_t send_info = { 0 };

    strcpy(uconf.cmd, UCTL_CONF_CMD_UPLOAD);
    uconf.rate = cli_conf->target_rate;
    uconf.duration = cli_conf->duration;
    uconf.blksize = PMTU;

    if (!cli_ctl_snd(udp_fd, &uconf, &resp_uconf, DEFAULT_RETRY_COUNT))
        return 0;

    gettimeofday(&start, NULL);
    ms_start = TV2MS(&start);
    ms_end = ms_start + uconf.duration;
    if (!strcmp(resp_uconf.cmd, UCTL_CONF_CMD_ACK)) {
        while (1) {
            if (!ratelimit(TV2US(&start), send_info.bytes, uconf.rate))
                continue;
            ret = write(udp_fd, buffer, uconf.blksize);
            if (ret < 0) {
                log_syserr("upload write error.");
                return 0;
            }
            send_info.bytes += ret;
            send_info.packets++;

            gettimeofday(&now, NULL);
            ms_now = TV2MS(&now);
            if (ms_now > ms_end) {
                break;
            }
        }
    }

    strcpy(uconf2.cmd, UCTL_CONF_CMD_UPLOAD_FINISH);
    if (!cli_ctl_snd(udp_fd, &uconf2, &resp_uconf, DEFAULT_RETRY_COUNT))
        return 0;
    if (!strcmp(resp_uconf.cmd, UCTL_CONF_CMD_UPLOAD_REPORT)) {
        send_info.rate = cal_rate(resp_uconf.bytes, uconf.duration);
        log_debug("send_info.rate:%.2f", send_info.rate);
        bps2str(send_info.rate, tmp_str);
        log_trace("rcv bytes:%llu", resp_uconf.bytes);
        log_trace("rcv packets:%llu", resp_uconf.packets);
        log_trace("send bytes:%llu", send_info.bytes);
        log_trace("send packets:%llu", send_info.packets);
        log_trace("lost percent:%.2f%%", Positive((1 - ((double)resp_uconf.packets / send_info.packets)) * 100));
        log_trace("rate:%s", tmp_str);
        return 1;
    }
    return 0;
}
static void cli_close(int fd)
{
    uctl_conf_t uconf = { 0 }, resp_uconf = { 0 };

    strcpy(uconf.cmd, UCTL_CONF_CMD_CLOSE);
    cli_ctl_snd(fd, &uconf, &resp_uconf, 5);
    if (!strcmp(resp_uconf.cmd, UCTL_CONF_CMD_CLOSE)) {
        return;
    }
}
int us_cli_run(udpspeed_cli_conf_t* cli_conf)
{
    int ret, pmtu;
    int ctl_fd = 0, udp_fd = 0, max_fd = 0;
    int rcv_timeout;
    uint16_t udp_port;
    fd_set rdfds;
    struct timeval tmo;
    char buffer[UDP_BUFFER_MAX] = { 0 };
    struct sockaddr_in peer_addr;
    uctl_conf_t uconf;

    ctl_fd = socket_dgram_new();
    sockaddr_set(&peer_addr, AF_INET, htons(cli_conf->ctl_port), cli_conf->svr_host);

    rcv_timeout = cli_conf->rcv_timeout ? 2 : cli_conf->rcv_timeout;
    sock_rcvtmo(ctl_fd, rcv_timeout, 0);
    if (0 > connect(ctl_fd, (struct sockaddr*)&peer_addr, sizeof(peer_addr))) {
        log_syserr("connect failed.");
        return 0;
    }

    if ((pmtu = check_pmtu(ctl_fd)) == 0) {
        log_warn("can not check pmtu, set PMTU 1432");
    } else {
        PMTU = pmtu;
        log_trace("set PMTU %d", PMTU);
    }

    if (!cli_request(ctl_fd, &udp_port, 5)) {
        return 0;
    }
    log_trace("Get udp port %d", udp_port);

    udp_fd = socket_dgram_new();
    sock_rcvtmo(udp_fd, rcv_timeout, 0);
    sockaddr_set(&peer_addr, AF_INET, htons(udp_port), cli_conf->svr_host);
    connect(udp_fd, (struct sockaddr*)&peer_addr, sizeof(peer_addr));

    if (cli_conf->direction & DIRECTION_DOWNLOAD) {
        int re_download = 1;
        while (re_download--) {
            if (!cli_download(udp_fd, cli_conf)) {
                log_trace("cli download failed.");
                return 0;
            }
        }
    }

    if (cli_conf->direction & DIRECTION_UPLOAD) {
        if (!cli_upload(udp_fd, cli_conf)) {
            log_trace("cli upload failed.");
            return 0;
        }
    }
    cli_close(udp_fd);
    return 1;
}